package io.ebeaninternal.server.deploy.meta;

import io.ebean.RawSql;
import io.ebean.annotation.Cache;
import io.ebean.annotation.DocStore;
import io.ebean.annotation.DocStoreMode;
import io.ebean.config.ServerConfig;
import io.ebean.config.TableName;
import io.ebean.config.dbplatform.IdType;
import io.ebean.config.dbplatform.PlatformIdGenerator;
import io.ebean.event.BeanFindController;
import io.ebean.event.BeanPersistController;
import io.ebean.event.BeanPersistListener;
import io.ebean.event.BeanPostConstructListener;
import io.ebean.event.BeanPostLoad;
import io.ebean.event.BeanQueryAdapter;
import io.ebean.event.changelog.ChangeLogFilter;
import io.ebean.text.PathProperties;
import io.ebean.util.CamelCaseHelper;
import io.ebeaninternal.api.ConcurrencyMode;
import io.ebeaninternal.server.core.CacheOptions;
import io.ebeaninternal.server.deploy.BeanDescriptor.EntityType;
import io.ebeaninternal.server.deploy.BeanDescriptorManager;
import io.ebeaninternal.server.deploy.ChainedBeanPersistController;
import io.ebeaninternal.server.deploy.ChainedBeanPersistListener;
import io.ebeaninternal.server.deploy.ChainedBeanPostConstructListener;
import io.ebeaninternal.server.deploy.ChainedBeanPostLoad;
import io.ebeaninternal.server.deploy.ChainedBeanQueryAdapter;
import io.ebeaninternal.server.deploy.IndexDefinition;
import io.ebeaninternal.server.deploy.InheritInfo;
import io.ebeaninternal.server.deploy.parse.DeployBeanInfo;
import io.ebeaninternal.server.idgen.UuidIdGenerator;

import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Describes Beans including their deployment information.
 */
public class DeployBeanDescriptor<T> {

  private static final Map<String, String> EMPTY_NAMED_QUERY = new HashMap<>();

  private static final Map<String, RawSql> EMPTY_RAW_MAP = new HashMap<>();

  private static class PropOrder implements Comparator<DeployBeanProperty> {

    @Override
    public int compare(DeployBeanProperty o1, DeployBeanProperty o2) {

      int v2 = o1.getSortOrder();
      int v1 = o2.getSortOrder();
      return (v1 < v2 ? -1 : (v1 == v2 ? 0 : 1));
    }
  }

  private static final PropOrder PROP_ORDER = new PropOrder();

  private static final String I_SCALAOBJECT = "scala.ScalaObject";

  private final ServerConfig serverConfig;

  private final BeanDescriptorManager manager;

  /**
   * Map of BeanProperty Linked so as to preserve order.
   */
  private LinkedHashMap<String, DeployBeanProperty> propMap = new LinkedHashMap<>();

  private Map<String, RawSql> namedRawSql;

  private Map<String, String> namedQuery;

  private EntityType entityType;

  private DeployBeanPropertyAssocOne<?> unidirectional;

  /**
   * Type of Identity generation strategy used.
   */
  private IdType idType;

  /**
   * Set to true if the identity is default for the platform.
   */
  private boolean idTypePlatformDefault;

  /**
   * The name of an IdGenerator (optional).
   */
  private String idGeneratorName;

  private PlatformIdGenerator idGenerator;

  /**
   * The database sequence name (optional).
   */
  private String sequenceName;

  private int sequenceInitialValue;

  private int sequenceAllocationSize;

  /**
   * Used with Identity columns but no getGeneratedKeys support.
   */
  private String selectLastInsertedId;

  /**
   * The concurrency mode for beans of this type.
   */
  private ConcurrencyMode concurrencyMode;

  private boolean updateChangesOnly;

  private List<IndexDefinition> indexDefinitions;

  /**
   * The base database table.
   */
  private String baseTable;

  private String baseTableAsOf;

  private String baseTableVersionsBetween;

  private String draftTable;

  private String[] dependentTables;

  private boolean historySupport;

  private boolean readAuditing;

  private boolean draftable;

  private boolean draftableElement;

  private TableName baseTableFull;

  private String[] properties;

  /**
   * The EntityBean type used to create new EntityBeans.
   */
  private final Class<T> beanType;

  private final List<BeanPersistController> persistControllers = new ArrayList<>();
  private final List<BeanPersistListener> persistListeners = new ArrayList<>();
  private final List<BeanQueryAdapter> queryAdapters = new ArrayList<>();
  private final List<BeanPostLoad> postLoaders = new ArrayList<>();
  private final List<BeanPostConstructListener> postConstructListeners = new ArrayList<>();

  private CacheOptions cacheOptions = CacheOptions.NO_CACHING;

  /**
   * If set overrides the find implementation. Server side only.
   */
  private BeanFindController beanFinder;

  /**
   * The table joins for this bean. Server side only.
   */
  private final ArrayList<DeployTableJoin> tableJoinList = new ArrayList<>(2);

  /**
   * Inheritance information. Server side only.
   */
  private InheritInfo inheritInfo;

  private String name;

  private ChangeLogFilter changeLogFilter;

  private String dbComment;

  /**
   * One of NONE, INDEX or EMBEDDED.
   */
  private boolean docStoreMapped;

  private DocStore docStore;

  private PathProperties docStorePathProperties;

  private String docStoreQueueId;

  private String docStoreIndexName;

  private String docStoreIndexType;

  private DocStoreMode docStorePersist;
  private DocStoreMode docStoreInsert;
  private DocStoreMode docStoreUpdate;
  private DocStoreMode docStoreDelete;

  private List<DeployBeanProperty> idProperties;

  /**
   * Construct the BeanDescriptor.
   */
  public DeployBeanDescriptor(BeanDescriptorManager manager, Class<T> beanType, ServerConfig serverConfig) {
    this.manager = manager;
    this.serverConfig = serverConfig;
    this.beanType = beanType;
  }

  /**
   * Return the DeployBeanInfo for the given bean class.
   */
  DeployBeanInfo<?> getDeploy(Class<?> cls) {
    return manager.getDeploy(cls);
  }

  /**
   * Return true if this beanType is an abstract class.
   */
  public boolean isAbstract() {
    return Modifier.isAbstract(beanType.getModifiers());
  }

  /**
   * Set to true for @History entity beans that have history.
   */
  public void setHistorySupport() {
    this.historySupport = true;
  }

  /**
   * Return true if this is an @History entity bean.
   */
  public boolean isHistorySupport() {
    return historySupport;
  }

  /**
   * Set read auditing on for this entity bean.
   */
  public void setReadAuditing() {
    readAuditing = true;
  }

  /**
   * Return true if read auditing is on for this entity bean.
   */
  public boolean isReadAuditing() {
    return readAuditing;
  }

  public void setDbComment(String dbComment) {
    this.dbComment = dbComment;
  }

  public String getDbComment() {
    return dbComment;
  }

  public void setDraftable() {
    draftable = true;
  }

  public boolean isDraftable() {
    return draftable;
  }

  public void setDraftableElement() {
    draftable = true;
    draftableElement = true;
  }

  public boolean isDraftableElement() {
    return draftableElement;
  }

  /**
   * Read the top level doc store deployment information.
   */
  public void readDocStore(DocStore docStore) {

    this.docStore = docStore;
    docStoreMapped = true;
    docStoreQueueId = docStore.queueId();
    docStoreIndexName = docStore.indexName();
    docStoreIndexType = docStore.indexType();
    docStorePersist = docStore.persist();
    docStoreInsert = docStore.insert();
    docStoreUpdate = docStore.update();
    docStoreDelete = docStore.delete();
    String doc = docStore.doc();
    if (!doc.isEmpty()) {
      docStorePathProperties = PathProperties.parse(doc);
    }
  }

  public boolean isScalaObject() {
    Class<?>[] interfaces = beanType.getInterfaces();
    for (Class<?> anInterface : interfaces) {
      String iname = anInterface.getName();
      if (I_SCALAOBJECT.equals(iname)) {
        return true;
      }
    }
    return false;
  }

  public DeployBeanTable createDeployBeanTable() {

    DeployBeanTable beanTable = new DeployBeanTable(getBeanType());
    beanTable.setBaseTable(baseTable);
    beanTable.setIdProperties(propertiesId());

    return beanTable;
  }

  public void setEntityType(EntityType entityType) {
    this.entityType = entityType;
  }

  public boolean isEmbedded() {
    return EntityType.EMBEDDED.equals(entityType);
  }

  public boolean isBaseTableType() {
    EntityType et = getEntityType();
    return EntityType.ORM.equals(et);
  }

  public boolean isDocStoreOnly() {
    return EntityType.DOC.equals(entityType);
  }

  public EntityType getEntityType() {
    if (entityType == null) {
      entityType = EntityType.ORM;
    }
    return entityType;
  }

  public void setSequenceInitialValue(int sequenceInitialValue) {
    this.sequenceInitialValue = sequenceInitialValue;
  }

  public void setSequenceAllocationSize(int sequenceAllocationSize) {
    this.sequenceAllocationSize = sequenceAllocationSize;
  }

  public int getSequenceInitialValue() {
    return sequenceInitialValue;
  }

  public int getSequenceAllocationSize() {
    return sequenceAllocationSize;
  }

  public String[] getProperties() {
    return properties;
  }

  public void setProperties(String[] props) {
    this.properties = props;
  }

  /**
   * Return the class type this BeanDescriptor describes.
   */
  public Class<T> getBeanType() {
    return beanType;
  }

  public void setChangeLogFilter(ChangeLogFilter changeLogFilter) {
    this.changeLogFilter = changeLogFilter;
  }

  public ChangeLogFilter getChangeLogFilter() {
    return changeLogFilter;
  }

  /**
   * Returns the Inheritance mapping information. This will be null if this type
   * of bean is not involved in any ORM inheritance mapping.
   */
  public InheritInfo getInheritInfo() {
    return inheritInfo;
  }

  /**
   * Set the ORM inheritance mapping information.
   */
  public void setInheritInfo(InheritInfo inheritInfo) {
    this.inheritInfo = inheritInfo;
  }

  /**
   * Enable L2 bean and query caching based on Cache annotation.
   */
  public void setCache(Cache cache) {

    String naturalKey = null;
    if (!cache.naturalKey().isEmpty()) {
      // find the property and mark as natural key property
      String propName = cache.naturalKey().trim();
      DeployBeanProperty beanProperty = getBeanProperty(propName);
      if (beanProperty != null) {
        beanProperty.setNaturalKey();
        naturalKey = propName;
      }
    }
    this.cacheOptions = new CacheOptions(cache, naturalKey);
  }

  /**
   * Return the cache options.
   */
  public CacheOptions getCacheOptions() {
    return cacheOptions;
  }

  public DeployBeanPropertyAssocOne<?> getUnidirectional() {
    return unidirectional;
  }

  public void setUnidirectional(DeployBeanPropertyAssocOne<?> unidirectional) {
    this.unidirectional = unidirectional;
  }

  /**
   * Return the concurrency mode used for beans of this type.
   */
  public ConcurrencyMode getConcurrencyMode() {
    return concurrencyMode;
  }

  /**
   * Set the concurrency mode used for beans of this type.
   */
  public void setConcurrencyMode(ConcurrencyMode concurrencyMode) {
    this.concurrencyMode = concurrencyMode;
  }

  public boolean isUpdateChangesOnly() {
    return updateChangesOnly;
  }

  public void setUpdateChangesOnly(boolean updateChangesOnly) {
    this.updateChangesOnly = updateChangesOnly;
  }

  /**
   * Add a compound unique constraint.
   */
  public void addIndex(IndexDefinition c) {
    if (indexDefinitions == null) {
      indexDefinitions = new ArrayList<>();
    }
    indexDefinitions.add(c);
  }

  /**
   * Return the compound unique constraints (can be null).
   */
  public IndexDefinition[] getIndexDefinitions() {
    if (indexDefinitions == null) {
      return null;
    } else {
      return indexDefinitions.toArray(new IndexDefinition[indexDefinitions.size()]);
    }
  }

  /**
   * Return the beanFinder. Usually null unless overriding the finder.
   */
  public BeanFindController getBeanFinder() {
    return beanFinder;
  }

  /**
   * Set the BeanFinder to use for beans of this type. This is set to override
   * the finding from the default.
   */
  public void setBeanFinder(BeanFindController beanFinder) {
    this.beanFinder = beanFinder;
  }

  /**
   * Return the BeanPersistController (could be a chain of them, 1 or null).
   */
  public BeanPersistController getPersistController() {
    if (persistControllers.isEmpty()) {
      return null;
    } else if (persistControllers.size() == 1) {
      return persistControllers.get(0);
    } else {
      return new ChainedBeanPersistController(persistControllers);
    }
  }

  /**
   * Return the BeanPersistListener (could be a chain of them, 1 or null).
   */
  public BeanPersistListener getPersistListener() {
    if (persistListeners.isEmpty()) {
      return null;
    } else if (persistListeners.size() == 1) {
      return persistListeners.get(0);
    } else {
      return new ChainedBeanPersistListener(persistListeners);
    }
  }

  public BeanQueryAdapter getQueryAdapter() {
    if (queryAdapters.isEmpty()) {
      return null;
    } else if (queryAdapters.size() == 1) {
      return queryAdapters.get(0);
    } else {
      return new ChainedBeanQueryAdapter(queryAdapters);
    }
  }

  /**
   * Return the BeanPostLoad (could be a chain of them, 1 or null).
   */
  public BeanPostLoad getPostLoad() {
    if (postLoaders.isEmpty()) {
      return null;
    } else if (postLoaders.size() == 1) {
      return postLoaders.get(0);
    } else {
      return new ChainedBeanPostLoad(postLoaders);
    }
  }

  /**
   * Return the BeanPostCreate(could be a chain of them, 1 or null).
   */
  public BeanPostConstructListener getPostConstructListener() {
    if (postConstructListeners.isEmpty()) {
      return null;
    } else if (postConstructListeners.size() == 1) {
      return postConstructListeners.get(0);
    } else {
      return new ChainedBeanPostConstructListener(postConstructListeners);
    }
  }

  public void addPersistController(BeanPersistController controller) {
    persistControllers.add(controller);
  }

  public void addPersistListener(BeanPersistListener listener) {
    persistListeners.add(listener);
  }

  public void addQueryAdapter(BeanQueryAdapter queryAdapter) {
    queryAdapters.add(queryAdapter);
  }

  public void addPostLoad(BeanPostLoad postLoad) {
    postLoaders.add(postLoad);
  }

  public void addPostConstructListener(BeanPostConstructListener postConstructListener) {
    postConstructListeners.add(postConstructListener);
  }

  public String getDraftTable() {
    return draftTable;
  }

  /**
   * For view based entity return the dependant tables.
   */
  public String[] getDependentTables() {
    return dependentTables;
  }

  /**
   * Return the base table. Only properties mapped to the base table are by
   * default persisted.
   */
  public String getBaseTable() {
    return baseTable;
  }

  /**
   * Return the base table with as of suffix.
   */
  public String getBaseTableAsOf() {
    return historySupport ? baseTableAsOf : baseTable;
  }

  /**
   * Return the base table with versions between suffix.
   */
  public String getBaseTableVersionsBetween() {
    return baseTableVersionsBetween;
  }

  /**
   * Return the base table with full structure.
   */
  public TableName getBaseTableFull() {
    return baseTableFull;
  }

  /**
   * Set when entity is based on a view.
   */
  public void setView(String viewName, String[] dependentTables) {
    this.entityType = EntityType.VIEW;
    this.dependentTables = dependentTables;
    setBaseTable(new TableName(viewName), "", "");
  }

  /**
   * Set the base table. Only properties mapped to the base table are by default persisted.
   */
  public void setBaseTable(TableName baseTableFull, String asOfSuffix, String versionsBetweenSuffix) {
    this.baseTableFull = baseTableFull;
    this.baseTable = baseTableFull == null ? null : baseTableFull.getQualifiedName();
    this.baseTableAsOf = baseTable + asOfSuffix;
    this.baseTableVersionsBetween = baseTable + versionsBetweenSuffix;
    this.draftTable = (draftable) ? baseTable + "_draft" : baseTable;
  }

  public void sortProperties() {

    ArrayList<DeployBeanProperty> list = new ArrayList<>(propMap.values());
    list.sort(PROP_ORDER);

    propMap = new LinkedHashMap<>(list.size());
    for (DeployBeanProperty aList : list) {
      addBeanProperty(aList);
    }
  }

  /**
   * Add a bean property.
   */
  public DeployBeanProperty addBeanProperty(DeployBeanProperty prop) {
    return propMap.put(prop.getName(), prop);
  }

  /**
   * Find the matching property for a given property name or dbColumn.
   * <p>
   * This is primarily to find imported primary key columns (ManyToOne that also match the PK).
   * </p>
   */
  DeployBeanProperty findMatch(String propertyName, String dbColumn) {

    DeployBeanProperty prop = propMap.get(propertyName);
    if (prop != null) {
      return prop;
    }
    if (dbColumn != null) {
      String asCamel = CamelCaseHelper.toCamelFromUnderscore(dbColumn);
      prop = propMap.get(asCamel);
      if (prop != null) {
        return prop;
      }
      // scan looking for dbColumn match
      for (DeployBeanProperty property : propMap.values()) {
        if (dbColumn.equals(property.getDbColumn())) {
          return property;
        }
      }
    }
    return null;
  }

  /**
   * Get a BeanProperty by its name.
   */
  public DeployBeanProperty getBeanProperty(String propName) {
    return propMap.get(propName);
  }

  /**
   * Return the bean class name this descriptor is used for.
   * <p>
   * If this BeanDescriptor is for a table then this returns the table name
   * instead.
   * </p>
   */
  public String getFullName() {
    return beanType.getName();
  }

  /**
   * Return the bean short name.
   */
  public String getName() {
    return name;
  }

  /**
   * Set the bean shortName.
   */
  public void setName(String name) {
    this.name = name;
  }

  /**
   * Return the identity generation type.
   */
  public IdType getIdType() {
    return idType;
  }

  /**
   * Set the identity generation type.
   */
  public void setIdType(IdType idType) {
    this.idType = idType;
  }

  /**
   * Set when the identity type is the platform default.
   */
  public void setIdTypePlatformDefault() {
    this.idTypePlatformDefault = true;
  }

  /**
   * Return true when the identity is the platform default.
   */
  public boolean isIdTypePlatformDefault() {
    return idTypePlatformDefault;
  }

  /**
   * Return the DB sequence name (can be null).
   */
  public String getSequenceName() {
    return sequenceName;
  }

  /**
   * Set the DB sequence name.
   */
  private void setSequenceName(String sequenceName) {
    this.sequenceName = sequenceName;
  }

  /**
   * Return the SQL used to return the last inserted Id.
   * <p>
   * Used with Identity columns where getGeneratedKeys is not supported.
   * </p>
   */
  public String getSelectLastInsertedId() {
    return selectLastInsertedId;
  }

  /**
   * Set the SQL used to return the last inserted Id.
   */
  public void setSelectLastInsertedId(String selectLastInsertedId) {
    this.selectLastInsertedId = selectLastInsertedId;
  }

  /**
   * Return the name of the IdGenerator that should be used with this type of
   * bean. A null value could be used to specify the 'default' IdGenerator.
   */
  public String getIdGeneratorName() {
    return idGeneratorName;
  }

  /**
   * Set the name of the IdGenerator that should be used with this type of bean.
   */
  public void setIdGeneratorName(String idGeneratorName) {
    this.idGeneratorName = idGeneratorName;
  }

  /**
   * Return the actual IdGenerator for this bean type (can be null).
   */
  public PlatformIdGenerator getIdGenerator() {
    return idGenerator;
  }

  /**
   * Set the actual IdGenerator for this bean type.
   */
  public void setIdGenerator(PlatformIdGenerator idGenerator) {
    this.idGenerator = idGenerator;
    if (idGenerator != null && idGenerator.isDbSequence()) {
      setSequenceName(idGenerator.getName());
    }
  }

  /**
   * Assign the standard UUID generator.
   */
  public void setUuidGenerator() {
    this.idType = IdType.EXTERNAL;
    this.idGeneratorName = UuidIdGenerator.AUTO_UUID;
    this.idGenerator = UuidIdGenerator.INSTANCE;
  }

  /**
   * Assign a custom external IdGenerator.
   */
  public void setCustomIdGenerator(PlatformIdGenerator idGenerator) {
    this.idType = IdType.EXTERNAL;
    this.idGeneratorName = idGenerator.getName();
    this.idGenerator = idGenerator;
  }

  /**
   * Summary description.
   */
  @Override
  public String toString() {
    return getFullName();
  }

  /**
   * Add a TableJoin to this type of bean. For Secondary table properties.
   */
  public void addTableJoin(DeployTableJoin join) {
    tableJoinList.add(join);
  }

  List<DeployTableJoin> getTableJoins() {
    return tableJoinList;
  }

  /**
   * Return a collection of all BeanProperty deployment information.
   */
  public Collection<DeployBeanProperty> propertiesAll() {
    return propMap.values();
  }

  /**
   * Return the defaultSelectClause using FetchType.LAZY and FetchType.EAGER.
   */
  public String getDefaultSelectClause() {

    StringBuilder sb = new StringBuilder();

    boolean hasLazyFetch = false;

    for (DeployBeanProperty prop : propMap.values()) {
      if (!prop.isTransient() && !(prop instanceof DeployBeanPropertyAssocMany<?>)) {
        if (prop.isFetchEager()) {
          sb.append(prop.getName()).append(",");
        } else {
          hasLazyFetch = true;
        }
      }
    }

    if (!hasLazyFetch) {
      return null;
    }
    String selectClause = sb.toString();
    if (selectClause.isEmpty()) {
      throw new IllegalStateException("Bean " + getFullName() + " has no properties?");
    }
    return selectClause.substring(0, selectClause.length() - 1);
  }

  /**
   * Return true if the primary key is a compound key or if it's database type
   * is non-numeric (and hence not suitable for db identity or sequence.
   */
  public boolean isPrimaryKeyCompoundOrNonNumeric() {

    List<DeployBeanProperty> ids = propertiesId();
    if (ids.size() != 1) {
      // compound key
      return true;
    }
    DeployBeanProperty p = ids.get(0);
    if (p instanceof DeployBeanPropertyAssocOne<?>) {
      return ((DeployBeanPropertyAssocOne<?>) p).isCompound();
    } else {
      return !p.isDbNumberType();
    }
  }

  /**
   * Return the Primary Key column assuming it is a single column (not
   * compound). This is for the purpose of defining a sequence name.
   */
  public String getSinglePrimaryKeyColumn() {
    List<DeployBeanProperty> ids = propertiesId();
    if (ids.size() == 1) {
      DeployBeanProperty p = ids.get(0);
      if (p instanceof DeployBeanPropertyAssoc<?>) {
        // its a compound primary key
        return null;
      } else {
        return p.getDbColumn();
      }
    }
    return null;
  }

  /**
   * Return the BeanProperty that make up the unique id.
   * <p>
   * The order of these properties can be relied on to be consistent if the bean
   * itself doesn't change or the xml deployment order does not change.
   * </p>
   */
  public List<DeployBeanProperty> propertiesId() {

    if (idProperties == null) {
      idProperties = new ArrayList<>(2);
      for (DeployBeanProperty prop : propMap.values()) {
        if (prop.isId()) {
          idProperties.add(prop);
        }
      }
    }
    return idProperties;
  }

  public DeployBeanPropertyAssocOne<?> findJoinToTable(String tableName) {

    List<DeployBeanPropertyAssocOne<?>> assocOne = propertiesAssocOne();
    for (DeployBeanPropertyAssocOne<?> prop : assocOne) {
      DeployTableJoin tableJoin = prop.getTableJoin();
      if (tableJoin != null && tableJoin.getTable().equalsIgnoreCase(tableName)) {
        return prop;
      }
    }
    return null;
  }

  /**
   * Return an Iterator of BeanPropertyAssocOne that are not embedded. These are
   * effectively joined beans. For ManyToOne and OneToOne associations.
   */
  public List<DeployBeanPropertyAssocOne<?>> propertiesAssocOne() {

    ArrayList<DeployBeanPropertyAssocOne<?>> list = new ArrayList<>();

    for (DeployBeanProperty prop : propMap.values()) {
      if (prop instanceof DeployBeanPropertyAssocOne<?>) {
        if (!prop.isEmbedded()) {
          list.add((DeployBeanPropertyAssocOne<?>) prop);
        }
      }
    }

    return list;

  }

  /**
   * Return BeanPropertyAssocMany for this descriptor.
   */
  public List<DeployBeanPropertyAssocMany<?>> propertiesAssocMany() {

    ArrayList<DeployBeanPropertyAssocMany<?>> list = new ArrayList<>();

    for (DeployBeanProperty prop : propMap.values()) {
      if (prop instanceof DeployBeanPropertyAssocMany<?>) {
        list.add((DeployBeanPropertyAssocMany<?>) prop);
      }
    }

    return list;
  }

  /**
   * base properties without the unique id properties.
   */
  public List<DeployBeanProperty> propertiesBase() {

    ArrayList<DeployBeanProperty> list = new ArrayList<>();

    for (DeployBeanProperty prop : propMap.values()) {
      if (!(prop instanceof DeployBeanPropertyAssoc<?>) && !prop.isId()) {
        list.add(prop);
      }
    }

    return list;
  }

  /**
   * Check the mapping for class inheritance
   */
  public void checkInheritanceMapping() {
    if (inheritInfo == null) {
      checkInheritance(getBeanType());
    }
  }

  /**
   * Check valid mapping annotations on the class hierarchy.
   */
  private void checkInheritance(Class<?> beanType) {

    Class<?> parent = beanType.getSuperclass();
    if (parent == null || Object.class.equals(parent)) {
      // all good
      return;
    }
    if (parent.isAnnotationPresent(Entity.class)) {
      String msg = "Checking " + getBeanType() + " and found " + parent + " that has @Entity annotation rather than MappedSuperclass?";
      throw new IllegalStateException(msg);
    }
    if (parent.isAnnotationPresent(MappedSuperclass.class)) {
      // continue checking
      checkInheritance(parent);
    }
  }

  public PathProperties getDocStorePathProperties() {
    return docStorePathProperties;
  }

  /**
   * Return true if this type is mapped for a doc store.
   */
  public boolean isDocStoreMapped() {
    return docStoreMapped;
  }

  public String getDocStoreQueueId() {
    return docStoreQueueId;
  }

  public String getDocStoreIndexName() {
    return docStoreIndexName;
  }

  public String getDocStoreIndexType() {
    return docStoreIndexType;
  }

  public DocStore getDocStore() {
    return docStore;
  }

  /**
   * Return the DocStore index behavior for bean inserts.
   */
  public DocStoreMode getDocStoreInsertEvent() {
    return getDocStoreIndexEvent(docStoreInsert);
  }

  /**
   * Return the DocStore index behavior for bean updates.
   */
  public DocStoreMode getDocStoreUpdateEvent() {
    return getDocStoreIndexEvent(docStoreUpdate);
  }

  /**
   * Return the DocStore index behavior for bean deletes.
   */
  public DocStoreMode getDocStoreDeleteEvent() {
    return getDocStoreIndexEvent(docStoreDelete);
  }

  private DocStoreMode getDocStoreIndexEvent(DocStoreMode mostSpecific) {
    if (!docStoreMapped) {
      return DocStoreMode.IGNORE;
    }
    if (mostSpecific != DocStoreMode.DEFAULT) return mostSpecific;
    if (docStorePersist != DocStoreMode.DEFAULT) return docStorePersist;
    return serverConfig.getDocStoreConfig().getPersist();
  }

  /**
   * Return the named ORM queries.
   */
  public Map<String, String> getNamedQuery() {
    return (namedQuery != null) ? namedQuery : EMPTY_NAMED_QUERY;
  }

  /**
   * Add a named query.
   */
  public void addNamedQuery(String name, String query) {
    if (namedQuery == null) {
      namedQuery = new LinkedHashMap<>();
    }
    namedQuery.put(name, query);
  }

  /**
   * Return the named RawSql queries.
   */
  public Map<String, RawSql> getNamedRawSql() {
    return (namedRawSql != null) ? namedRawSql : EMPTY_RAW_MAP;
  }

  /**
   * Add a named RawSql from ebean.xml file.
   */
  public void addRawSql(String name, RawSql rawSql) {
    if (namedRawSql == null) {
      namedRawSql = new HashMap<>();
    }
    namedRawSql.put(name, rawSql);
  }
}
